using System;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Threading;

namespace OpenWaves.Caching
{
    public class AsyncConcurrentCache<T> : IConcurrentCache<T> where T : class
    {
        private readonly ICache<AsyncCacheEntry<T>> cache;
        private readonly Func<ConcurrentCacheEntry<T>, DateTime> softExpirationStrategy;
        private readonly Func<ConcurrentCacheEntry<T>, DateTime> hardExpirationStrategy;
        private readonly IThreadPool threadPool;
        private readonly ILockManager lockManager;

        public event EventHandler<AsyncOperationFailedEventArgs<T>> AsyncOperationFailed = delegate { };

        private static DateTime DefaultSoftExpirationStrategy(ConcurrentCacheEntry<T> entry)
        {
            var hardExpiration = entry.AbsoluteExpiration.HasValue ? (entry.AbsoluteExpiration.Value - Time.Now) : TimeSpan.FromHours(1);
            return Time.Now.AddTicks(hardExpiration.Ticks/2);
        }

        private static DateTime DefaultHardExpirationStrategy(ConcurrentCacheEntry<T> entry)
        {
            return entry.AbsoluteExpiration ?? Time.Now + TimeSpan.FromHours(0.1);            
        }

        public AsyncConcurrentCache(ICache<AsyncCacheEntry<T>> cache, ILockManager lockManager = null, IThreadPool threadPool = null)
            : this(cache, DefaultSoftExpirationStrategy, DefaultHardExpirationStrategy, lockManager, threadPool)
        {
        }

        public AsyncConcurrentCache(ICache<AsyncCacheEntry<T>> cache, TimeSpan softExpiration, TimeSpan hardExpiration, ILockManager lockManager = null, IThreadPool threadPool = null)
            : this(cache, entry => softExpiration == TimeSpan.MaxValue ? DateTime.MaxValue : Time.Now + softExpiration, entry => hardExpiration == TimeSpan.MaxValue ? DateTime.MaxValue : Time.Now + hardExpiration, lockManager, threadPool)
        {
        }

        public AsyncConcurrentCache(ICache<AsyncCacheEntry<T>> cache, Func<ConcurrentCacheEntry<T>, DateTime> softExpirationStrategy, Func<ConcurrentCacheEntry<T>, DateTime> hardExpirationStrategy, ILockManager lockManager = null, IThreadPool threadPool = null) 
        {
            this.cache = cache;
            this.softExpirationStrategy = softExpirationStrategy;
            this.hardExpirationStrategy = hardExpirationStrategy;
            this.threadPool = threadPool ?? new SystemThreadPool();
            this.lockManager = lockManager ?? new MonitorBasedLockManager();
        }

        public T GetOrCreate(string key, Func<ConcurrentCacheEntry<T>> create)
        {
            var entry = this.cache.GetValue(key);
            if (entry == null)
            {
                this.lockManager.AcquireLock(key);
                try
                {
                    entry = this.GetOrCreateInternal(key, create);

                    Contract.Assert(entry != null);
                }
                finally
                {
                    this.lockManager.ReleaseLock(key);
                }
            }
            
            if (entry.IsExpired)
            {
                if (this.lockManager.TryAcquireLock(key))
                {
                    this.threadPool.QueueWorkItem(() =>
                                                      {
                                                          try
                                                          {
                                                              this.RefreshValue(key, entry, create);
                                                          }
                                                          finally
                                                          {
                                                              this.lockManager.ReleaseLock(key);
                                                          }
                                                      });

                }
            }

            if (entry.Exception != null)
                throw new AggregateException(entry.Exception);

            return entry.Value;
        }

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void RefreshValue(string internalKey, AsyncCacheEntry<T> staleEntry, Func<ConcurrentCacheEntry<T>> create)
        {
            try
            {
                var newEntry = create();
                this.InsertValue(internalKey, newEntry.Value, this.hardExpirationStrategy(newEntry));
            }
            catch(Exception e)
            {
                Log.Error(e, "Failed to refresh async cache entry. Key: {0}", internalKey);

                if (e is ThreadAbortException || e is OutOfMemoryException)
                    throw;

                this.cache.InsertValue(
                    internalKey, 
                    this.CreateFailedEntry(staleEntry, e), 
                    DateTime.MaxValue);
            }
        }

        private AsyncCacheEntry<T> CreateFailedEntry(AsyncCacheEntry<T> staleEntry, Exception exception)
        {
            var eventArgs = new AsyncOperationFailedEventArgs<T>(
                staleEntry.Value, 
                Time.Now + TimeSpan.FromMinutes(1), 
                exception);

            this.AsyncOperationFailed(this, eventArgs);

            return new AsyncCacheEntry<T>(eventArgs.Value, eventArgs.AbsoluteExpiration, eventArgs.Exception);
        }

        private AsyncCacheEntry<T> GetOrCreateInternal(string internalKey, Func<ConcurrentCacheEntry<T>> create)
        {
            var entry = this.cache.GetValue(internalKey);
            if (entry == null)
            {
                var newEntry = create();

                entry = new AsyncCacheEntry<T>(
                    newEntry.Value,
                    this.softExpirationStrategy(newEntry));

                this.cache.InsertValue(internalKey, entry, this.hardExpirationStrategy(newEntry));
            }

            return entry;
        }

        public void InsertValue(string key, T value, DateTime absoluteExpiration)
        {
            var entry = new ConcurrentCacheEntry<T>(value, absoluteExpiration);

            this.cache.InsertValue(
                key, 
                new AsyncCacheEntry<T>(value, this.softExpirationStrategy(entry)), 
                absoluteExpiration);
        }

        public void EvictValue(string key)
        {
            this.cache.EvictValue(key);
        }

        public T GetValue(string key)
        {
            var entry = this.cache.GetValue(key);
            if (entry == null)
                return null;
            
            return entry.Value;
        }
    }

    [Serializable]
    public class AsyncCacheEntry<T> : CacheEntry<T> where T : class
    {
        public Exception Exception { get; private set; }

        public AsyncCacheEntry(T value, DateTime absoluteExpiration)
            : base(value, absoluteExpiration)
        {
        }

        public AsyncCacheEntry(T value) : base(value)
        {
        }

        public AsyncCacheEntry(T value, DateTime absoluteExpiration, Exception exception) : base(value, absoluteExpiration)
        {
            this.Exception = exception;
        }
    }

    public class AsyncOperationFailedEventArgs<T> : EventArgs where T : class
    {
        public T Value { get; set; }
        public DateTime AbsoluteExpiration { get; set; }
        public Exception Exception { get; set; }

        public AsyncOperationFailedEventArgs(T value, DateTime absoluteExpiration, Exception exception)
        {
            this.Value = value;
            this.AbsoluteExpiration = absoluteExpiration;
            this.Exception = exception;
        }
    }
}